require 'optparse'
require 'fileutils'
require 'logger'

require File.dirname(__FILE__) + '/usage'

class Installr::Client::Runner
  attr_reader   :namespace
  attr_reader   :action
  attr_reader   :args
  attr_reader   :options
  attr_reader   :storage

  def self.run(args)
    new(args).run
  end

  def initialize(args=[])
    @namespace = nil
    @help_only = nil
    @args = args
    @options = { :verbose => false, :debug => 2, :data_base_dir => File.expand_path(ENV['HOME']+'/Installr/data/'), :log_base_dir => File.expand_path(ENV['HOME']+'/Installr/logs/'), :build_path => '.', :no_depend => false, :remote_log_length => 10, :initialize => false }
    @exit_status = true
    @storage = nil
  end

  def parse_options!
    opts_parser = OptionParser.new do |opts|
      opts.banner = "\nAvailable options:\n\n"

      opts.on('-u USERNAME', '--user USERNAME')         { |user|  @options[:user] = user }
      opts.on('-p PASSWORD', '--password PASSWORD')     { |pass|  @options[:password] = pass }
      opts.on('--pass PASSWORD')                        { |pass|  @options[:password] = pass }
      opts.on('--auth-file AUTH_FILE')                  { |auth_file|  @options[:auth_file] = auth_file }
      opts.on('--passphrase PASSPHRASE')                { |passphrase|  @options[:passphrase] = passphrase }
      opts.on('--build-path PATH')                      { |build_path|  @options[:build_path] = build_path }
      opts.on('--scenario-file SCENARIO_FILE')          { |scenario_file|  @options[:scenario_file] = scenario_file }
      opts.on('--schedule CRON_FORMAT')                 { |cron_format|   @options[:cron_format] = cron_format }
      opts.on('--no-depend')                            {         @options[:no_depend] = true }
      opts.on('--script-file SCRIPT_FILE')              { |script_file|  @options[:script_file] = script_file }
      opts.on('--local-upload-path LOCAL_UPLOAD_PATH')  { |local_upload_path|  @options[:local_upload_path] = local_upload_path }
      opts.on('--remote-upload-path REMOTE_UPLOAD_PATH')      { |remote_upload_path|  @options[:remote_upload_path] = remote_upload_path }
      opts.on('--remote-port REMOTE_PORT')              { |remote_port|  @options[:remote_port] = remote_port }
      opts.on('--application-server-log')               {         @options[:application_server_log] = true }
      opts.on('--remote-log-file LOG_FILE')             { |remote_log_file|  @options[:remote_log_file] = remote_log_file }
      opts.on('--remote-log-length LENGTH')             { |remote_log_length|  @options[:remote_log_length] = remote_log_length }
      opts.on('--export-path EXPORT_ZIP_FILE')          { |export_zip_file|  @options[:export_path] = export_zip_file }
      opts.on('--import-path IMPORT_ZIP_FILE')          { |import_zip_file|  @options[:import_path] = import_zip_file }
      opts.on('--initialize')                           {         @options[:initialize] = true }

      opts.on('-d [LEVEL]', '--debug [LEVEL] (1: FATAL, 2: ERROR, 3: INFO, 4: UNKNOWN, 5: WARN, 6: DEBUG)')          { |level|  @options[:debug] = level }
      opts.on('-q', '--quiet')                          {         @options[:quiet] = true }
      opts.on('--verbose')                              {         @options[:verbose] = true }
      opts.on('-v', '--version')                        {         set_command(:system, :version) }
      opts.on('-h', '--help')                           {         puts "#{command_usage}\n"; exit }
      opts.on('--data-base-dir DATA_BASE_DIR')          { |data_base_dir|  @options[:data_base_dir] = data_base_dir }
      opts.on('--log-base-dir LOG_BASE_DIR')            { |log_base_dir|   @options[:log_base_dir] = log_base_dir }

      opts.on_tail('--options')                         {         puts "#{opts}\n"; exit }
    end
    @args = opts_parser.parse!(@args)
    self
  end

  def display_help
    puts command_usage
    exit
  end

  def set_command(namespace, action, args_range=0)
    return if @help_only
    if args_range == 999
      usage_error("Syntax error for [#{action}]")
      @exit_status = false
      return
    end

    unless args_range == "*" || args_range.is_a?(Range)
      args_range = (args_range.to_i..args_range.to_i)
    end

    if args_range == "*" || args_range.include?(@args.size)
      @namespace = namespace
      @action    = action
    else
      @exit_status = false
      if @args.size > args_range.last
        usage_error("Too many arguments for [#{action}]: %s" % [ @args[args_range.last..-1].map{|a| "'#{a}'"}.join(', ') ])
      else
        usage_error("Not enough arguments for [#{action}]")
      end
    end
  end

  def parse_command!
    return if @namespace && @action

    cmd = @args.shift

    case cmd
    when 'create'
      usage('Installr create agent/group/scenario/schedule')
      sub_cmd = @args.shift
      case sub_cmd
        when 'agent'
          usage('Installr create agent <agent_name> <agent_ip_address> [--user <user_name> --password <user_password> --remote-port <remote_port>  --remote-upload-path <remote-upload-path>] or Installr create agent <agent_name> <agent_ip_address> --auth-file <auth-file> [--user <user_name> --passphrase <passphrase> --remote-port <remote_port>  --remote-upload-path <remote-upload-path>')
          set_command(:agent, :create, 2)

        when 'group'
          usage('Installr create group <group_name> <group description>')
          set_command(:group, :create, 2)

        when 'scenario'
          usage('Installr create scenario <scenario_name> --scenario-file <scenario-file>')
          set_command(:scenario, :create, 1)

        when 'schedule'
          usage('Installr create schedule <schedule_name> --schedule <cron_format>')
          set_command(:schedule, :create, 1)
      end

    when 'delete'
      usage('Installr delete agent/group/scenario/schedule')
      sub_cmd = @args.shift
      case sub_cmd
        when 'agent'
          usage('Installr delete agent <agent_name>')
          set_command(:agent, :delete, 1)

        when 'group'
          usage('Installr delete group <group_name>')
          set_command(:group, :delete, 1)

        when 'scenario'
          usage('Installr delete scenario <scenario_name>')
          set_command(:scenario, :delete, 1)

        when 'schedule'
          usage('Installr delete schedule <schedule_name>')
          set_command(:schedule, :delete, 1)
      end

    when 'info'
      usage('Installr info agent/group/head/scenario/schedule')
      sub_cmd = @args.shift
      case sub_cmd
        when 'agent'
          usage('Installr info agent <agent_name>')
          set_command(:agent, :info, 1)

        when 'group'
          usage('Installr info group <group_name>')
          set_command(:group, :info, 1)

        when 'head'
          if (@args[1] == "from") and (@args[2] == "local")
            usage('Installr info head <app_name> from local')
	    @args.slice!(1)
	    @args.slice!(1)
            set_command(:deploy, :info_head_from_local, 1)
          end
          if (@args[1] == "from") and (@args[2] == "group")
            usage('Installr info head <app_name> from group <group_name>')
	    @args.slice!(1)
	    @args.slice!(1)
            set_command(:deploy, :info_head_from_group, 2)
          end
          if (@args[1] == "from") and (@args[2] == "agent")
            usage('Installr info head <app_name> from agent <agent_name>')
	    @args.slice!(1)
	    @args.slice!(1)
            set_command(:deploy, :info_head_from_agent, 2)
          end

        when 'scenario'
          usage('Installr info scenario <scenario_name>')
          set_command(:scenario, :info, 1)

        when 'schedule'
          usage('Installr info schedule <schedule_name>')
          set_command(:schedule, :info, 1)
      end

    when 'list'
      usage('Installr list agents/groups/apps/scenarios/schedules')
      sub_cmd = @args.shift
      case sub_cmd
        when 'agents'
          if @args.size == 0
            usage('Installr list agents')
            set_command(:agent, :list, 0)
          elsif @args.size == 3
            usage('Installr list agents from group <group_name>')
            if (@args[0] == "from") and (@args[1] == "group")
              @args.slice!(0)
              @args.slice!(0)
              set_command(:agent, :list_from_group, 1)
            else
              # force to error
              set_command(:agent, :list_from_group, 999)
            end
          else
            usage('Installr list agents or Installr list agents from group <group_name>')
            set_command(:agent, :list_from_group, 999)
          end

        when 'apps'
          usage('Installr list apps')
          set_command(:deploy, :apps, 0)

        when 'groups'
          usage('Installr list groups')
          set_command(:group, :list, 0)

        when 'scenarios'
          if @args.size == 0
            usage('Installr list scenarios')
            set_command(:scenario, :list, 0)
          elsif @args.size == 3
            usage('Installr list scenarios from schedule <schedule_name>')
            if (@args[0] == "from") and (@args[1] == "schedule")
              @args.slice!(0)
              @args.slice!(0)
              set_command(:scenario, :list_from_schedule, 1)
            else
              # force to error
              set_command(:scenario, :list_from_schedule, 999)
            end
          else
            usage('Installr list scenarios or Installr list scenarios from schedule <schedule_name>')
            set_command(:scenario, :list_from_schedule, 999)
          end

        when 'schedules'
          usage('Installr list schedules')
          set_command(:schedule, :list, 0)
      end

    when 'add'
      usage('Installr add agent/scenario')
      sub_cmd = @args.shift
      case sub_cmd
        when 'agent'
          usage('Installr add agent <agent_name> to group <group_name>')
          if (@args[1] == "to") and (@args[2] == "group")
            @args.slice!(1)
            @args.slice!(1)
            set_command(:agent, :add, 2)
          else
            # force to error
            set_command(:agent, :add, 999)
          end
	when 'scenario'
          usage('Installr add scenario <scenario_name> to schedule <schedule_name>')
          if (@args[1] == "to") and (@args[2] == "schedule")
            @args.slice!(1)
            @args.slice!(1)
            set_command(:scenario, :add, 2)
          else
            # force to error
            set_command(:scenario, :add, 999)
          end
      end

    when 'remove'
      usage('Installr remove agent/scenario')
      sub_cmd = @args.shift
      case sub_cmd
        when 'agent'
          usage('Installr remove agent <agent_name> from group <group_name>')
          if (@args[1] == "from") and (@args[2] == "group")
            @args.slice!(1)
            @args.slice!(1)
            set_command(:agent, :remove, 2)
          else
            # force to error
            set_command(:agent, :remove, 999)
          end
	when 'scenario'
          usage('Installr remove scenario <scenario_name> from schedule <schedule_name>')
          if (@args[1] == "from") and (@args[2] == "schedule")
            @args.slice!(1)
            @args.slice!(1)
            set_command(:scenario, :remove, 2)
          else
            # force to error
            set_command(:scenario, :remove, 999)
          end
      end

    when 'deploy'
      usage('Installr deploy <app_name> <war_file> to group <group_name> or Installr deploy <app_name> <war_file> to agent <agent_name>')
      if (@args[2] == "to") and (@args[3] == "group")
        @args.slice!(2)
        @args.slice!(2)
        set_command(:deploy, :deploy_group, 3)
      elsif (@args[2] == "to") and (@args[3] == "agent")
        @args.slice!(2)
        @args.slice!(2)
        set_command(:deploy, :deploy_agent, 3)
      else
        # force to error
        set_command(:deploy, :deploy_group, 999)
      end

    when 'undeploy'
      usage('Installr undeploy <app_name> from group <group_name> or Installr undeploy <app_name> from agent <agent_name>')
      if (@args[1] == "from") and (@args[2] == "group")
        @args.slice!(1)
        @args.slice!(1)
        set_command(:deploy, :undeploy_group, 2)
      elsif (@args[1] == "from") and (@args[2] == "agent")
        @args.slice!(1)
        @args.slice!(1)
        set_command(:deploy, :undeploy_agent, 2)
      else
        # force to error
        set_command(:deploy, :undeploy_group, 999)
      end

    when 'activate'
      usage('Install activate schedule <schedule_name> or Installr activate <app_name> from group <group_name> or Installr activate <app_name> from agent <agent_name>')
      if @args[0] == "schedule"
	@args.slice!(0)
        set_command(:schedule, :activate, 1)
      else
        if (@args[1] == "from") and (@args[2] == "group")
          @args.slice!(1)
          @args.slice!(1)
          set_command(:deploy, :activate_group, 2)
        elsif (@args[1] == "from") and (@args[2] == "agent")
          @args.slice!(1)
          @args.slice!(1)
          set_command(:deploy, :activate_agent, 2)
        else
          # force to error
          set_command(:deploy, :activate_group, 999)
	end
      end

    when 'deactivate'
      usage('Installr deactivate <schedule_name> or Installr deactivate <app_name> from group <group_name> or Installr deactivate <app_name> from agent <agent_name>')
      if @args[0] == "schedule"
	@args.slice!(0)
        set_command(:schedule, :deactivate, 1)
      else
        if (@args[1] == "from") and (@args[2] == "group")
          @args.slice!(1)
          @args.slice!(1)
          set_command(:deploy, :deactivate_group, 2)
        elsif (@args[1] == "from") and (@args[2] == "agent")
          @args.slice!(1)
          @args.slice!(1)
          set_command(:deploy, :deactivate_agent, 2)
        else
          # force to error
          set_command(:deploy, :deactivate_group, 999)
	end
      end

    when 'start'
      usage('Installr start <app_name> from group <group_name> or Installr start <app_name> from agent <agent_name>')
      if (@args[1] == "from") and (@args[2] == "group")
        @args.slice!(1)
        @args.slice!(1)
        set_command(:deploy, :start_group, 2)
      elsif (@args[1] == "from") and (@args[2] == "agent")
        @args.slice!(1)
        @args.slice!(1)
        set_command(:deploy, :start_agent, 2)
      else
        # force to error
        set_command(:deploy, :start_group, 999)
      end

    when 'stop'
      usage('Installr stop <app_name> from group <group_name> or Installr stop <app_name> from agent <agent_name>')
      if (@args[1] == "from") and (@args[2] == "group")
        @args.slice!(1)
        @args.slice!(1)
        set_command(:deploy, :stop_group, 2)
      elsif (@args[1] == "from") and (@args[2] == "agent")
        @args.slice!(1)
        @args.slice!(1)
        set_command(:deploy, :stop_agent, 2)
      else
        # force to error
        set_command(:deploy, :stop_group, 999)
      end

    when 'history'
      usage('Installr history <app_name> from group <group_name> or Installr history <app_name> from agent <agent_name> or Installr history <app_name> from local [--initialize]')
      if (@args[1] == "from") and (@args[2] == "group")
        @args.slice!(1)
        @args.slice!(1)
        set_command(:deploy, :history_group, 2)
      elsif (@args[1] == "from") and (@args[2] == "agent")
        @args.slice!(1)
        @args.slice!(1)
        set_command(:deploy, :history_agent, 2)
      elsif (@args[1] == "from") and (@args[2] == "local")
        @args.slice!(1)
        @args.slice!(1)
        set_command(:deploy, :history_local, 1)
      else
        # force to error
        set_command(:deploy, :history_group, 999)
      end

    when 'set'
      usage('Installr set head <app_name> <app_md5hash> to group <group_name> or Installr set head <app_name> <app_md5hash> to agent <agent_name>')
      if (@args[0] == "head") and (@args[3] == "to") and (@args[4] == "group")
        @args.slice!(0)
        @args.slice!(2)
        @args.slice!(2)
        set_command(:deploy, :head_group, 3)
      elsif (@args[0] == "head") and (@args[3] == "to") and (@args[4] == "agent")
	@args.slice!(0)
        @args.slice!(2)
        @args.slice!(2)
        set_command(:deploy, :head_agent, 3)
      else
        # force to error
        set_command(:deploy, :head_group, 999)
      end

    when 'make'
      usage('Installr make checkout/build')
      sub_cmd = @args.shift
      case sub_cmd
        when 'checkout'
          usage('Installr make checkout --build-path <app_base_path> using <build_system> <target_rule>')
          @args.slice!(0)
          set_command(:build, :checkout, 2)
        when 'build'
          usage('Installr make build --build-path <app_base_path> [--no-depend] using <build_system> <target_rule>')
          @args.slice!(0)
          set_command(:build, :make, 2)
      end

    when 'execute'
      usage('Installr execute scenario/schedules/bootstrap')
      sub_cmd = @args.shift
      case sub_cmd
        when 'scenario'
          usage('Installr execute scenario <scenario_name>')
          set_command(:scenario, :execute, 1)

        when 'schedules'
          usage('Installr execute schedules')
          set_command(:schedule, :execute)

        when 'bootstrap'
          usage('Installr execute bootstrap group/agent')
          sub_cmd_target = @args.shift
          case sub_cmd_target
            when 'group'
              usage('Installr execute bootstrap group <group_name>')
              set_command(:group, :execute_bootstrap, 1)

            when 'agent'
              usage('Installr execute bootstrap agent <agent_name>')
              set_command(:agent, :execute_bootstrap, 1)
          end
      end

    when 'show'
      usage('Installr show status from group/agent')
      sub_cmd = @args.shift
      case sub_cmd
        when 'status'
          sub_cmd_target = @args.shift
          if sub_cmd_target != "from"
            # force to error
            set_command(:group, :show_status, 999)
          end
          sub_cmd_target = @args.shift
          case sub_cmd_target
            when 'group'
              usage('Installr show status from group <group_name>')
              set_command(:group, :show_status, 1)

            when 'agent'
              usage('Installr show status from agent <agent_name>')
              set_command(:agent, :show_status, 1)
          end
        when 'log'
          usage('Installr show log from agent <agent_name> [--application-server-log --remote-log-file <remote-log-file> --remote-log-length <remote-log-length>or Installr show log from group <group_name> [--application-server-log --remote-log-file <remote-log-file> --remote-log-length <remote-log-length>')
          if (@args[0] == "from") and (@args[1] == "agent")
            @args.slice!(0)
            @args.slice!(0)
            set_command(:agent, :log, 1)
          elsif (@args[0] == "from") and (@args[1] == "group")
            @args.slice!(0)
            @args.slice!(0)
            set_command(:group, :log, 1)
          else
            # force to error
            set_command(:agent, :log, 999)
          end
      end

    when 'export'
      usage('Installr export database from local --export-path <export-zip-file> [--initialize]')
      if (@args[0] == "database") and (@args[1] == "from") and (@args[2] == "local")
        @args.slice!(0)
        @args.slice!(0)
        @args.slice!(0)
        set_command(:system, :export, 0)
      else
        # force to error
        set_command(:system, :export, 999)
      end

    when 'import'
      usage('Installr import database to local --import-path <import-zip-file> [--initialize]')
      if (@args[0] == "database") and (@args[1] == "to") and (@args[2] == "local")
        @args.slice!(0)
        @args.slice!(0)
        @args.slice!(0)
        set_command(:system, :import, 0)
      else
        # force to error
        set_command(:system, :import, 999)
      end

    when 'version'
      usage('Installr version')
      set_command(:system, :version)

    when 'help'
      display_help if @args.size == 0
      @help_only = true
      parse_command!

    when 'usage'
      puts basic_usage
      exit(true)

    when 'options'
      @args = @args.unshift('--options')
      parse_options!

    else
      if cmd
        puts "Installr: Unknown command [#{cmd}]"
        puts basic_usage
        exit(false)
      end
    end
  end

  def usage(msg = nil)
    @usage = msg if msg
    @usage
  end

  def usage_error(msg = nil)
    @usage_error = msg if msg
    @usage_error
  end

  def run
    trap('TERM') { print "\nTerminated\n"; exit(false)}

    parse_options!
    parse_command!

    FileUtils.mkdir_p(@options[:log_base_dir]) unless File.directory?(@options[:log_base_dir])
    @log = Logger.new(@options[:log_base_dir]+'/Installr.log', 'daily')
    case @options[:debug]
      when 1 then @log.level = Logger::FATAL
      when 2 then @log.level = Logger::ERROR
      when 3 then @log.level = Logger::INFO
      when 4 then @log.level = Logger::UNKNOWN
      when 5 then @log.level = Logger::WARN
      when 6 then @log.level = Logger::DEBUG
      else @log.level = Logger::DEBUG
    end

    @storage = Installr::Database::Storage.new(@log, @options)
    @storage.open

    if @namespace && @action
      result = Installr::Client::Command.const_get(@namespace.to_s.capitalize).new(@storage, @log, @options).send(@action, *@args.collect(&:dup))
      if result == 0
        @exit_status = true
      elsif result == -2
        # maybe transient network problem: try again and check the result
        puts "retrying command #{@namespace.to_s.capitalize} once again"
        @log.warn("Installr: retrying command #{@namespace.to_s.capitalize} once again")
        result = Installr::Client::Command.const_get(@namespace.to_s.capitalize).new(@storage, @log, @options).send(@action, *@args.collect(&:dup))
        if result == 0
          @exit_status = true
        else
          @exit_status = false
        end
      else
        @exit_status = false
      end
    elsif @help_only || @usage
      display_usage
    else
      puts basic_usage
      exit(false)
    end

  rescue OptionParser::InvalidOption => e
    puts(e.message)
    puts(basic_usage)
    @exit_status = false
  rescue OptionParser::AmbiguousOption => e
    puts(e.message)
    puts(basic_usage)
    @exit_status = false
  rescue SystemExit => e
    @exit_status = e.success?
  rescue SyntaxError => e
    puts e.message
    puts e.backtrace
    @exit_status = false
  rescue Interrupt => e
    puts "\nInstallr Interrupted" 
    @exit_status = false
  rescue Exception => e
    puts e.message
    puts e.backtrace
    @exit_status = false
  ensure
    @exit_status == true if @exit_status.nil?
    if @options[:verbose]
      @namespace = 'Installr' unless @namespace 
      @action = 'main' unless @action 
      if @exit_status
        puts "[#{@namespace}:#{@action}] SUCCEEDED"
      else
        puts "[#{@namespace}:#{@action}] FAILED"
      end
    end
    exit(@exit_status)
  end
end
